package me.yuzho.study.concurrency.simulation;

import java.io.IOException;
import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.Queue;
import java.util.Random;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static net.mindview.util.Print.*;



// Read-only objects don't require synchronization
class Customer {
    private final int serviceTime;
    public Customer (int tm) { serviceTime = tm; } 
    public int getServiceTime() { return serviceTime; }
    @Override
    public String toString() {
        return "[" + serviceTime + "]"; 
    }
}

// Teach the customer line to display itself: 客户队列
// ArrayBlockingQueue: FIFO queue
@SuppressWarnings("serial")
class CustomerLine extends ArrayBlockingQueue<Customer> {

    public CustomerLine(int maxLineSize) {
        super(maxLineSize);
    }
    
    @Override
    public String toString() {
        if(this.size() == 0) return "[Empty]";
        
        StringBuilder result = new StringBuilder();
        for(Customer customer : this) {
            result.append(customer);
        }
        
        return result.toString();
    }
    
}

//Randomly add customers to a queue: 随机生成客户到队列中
class CustomerGenerator implements Runnable {
    
    private CustomerLine customers;
    private static Random rand = new Random(47);
    
    public CustomerGenerator(CustomerLine cq) {
        customers = cq;
    }

    @Override
    public void run() {
        try {
            while(!Thread.interrupted()) {
                // 每间隔 300ms添加一个顾客到队列中, 随机生成它的服务时间
                TimeUnit.MILLISECONDS.sleep(rand.nextInt(300));
                customers.put(new Customer(rand.nextInt(1000)));
            }
        } catch (InterruptedException e) {
           print("CustomerGenerator interrupted");
        }
        
        print("CustomerGenerator terminating");
    }  
}

class Teller implements Runnable, Comparable<Teller> {
    
    private static int counter = 0;
    private final int id = counter++;
    
    // Customers served during this shift
    private int customersServed = 0;
    private CustomerLine customers;
    private boolean servingCustomerLine = true;
    
    public Teller(CustomerLine cq) { customers = cq; }
    
    @Override
    public void run() {
        try {
            while(!Thread.interrupted()) {
               Customer customer = customers.take(); // Take a customer form customer queue
               TimeUnit.MILLISECONDS.sleep(customer.getServiceTime());
               
               synchronized (this) {
                   customersServed++;
                   while(!servingCustomerLine) { // 不是太理解
                       wait();
                   }
                
               }
            }
        } catch (InterruptedException e) {
            print(this + "interrupted");
        }
        print(this + " terminating");
    }
    
    public synchronized void doSomethingElse() {
        customersServed = 0;
        servingCustomerLine = false;
    }
    
    public synchronized void serveCustomerLine() {
        assert !servingCustomerLine: "already serving: " + this;
        servingCustomerLine = true;
        notifyAll();
    }
    
    @Override
    public String toString() {
        return "Teller " + id + " ";
    }
    
    public String shortString() { return "Teller " + id + " "; }
    
    // Used by priority queue
    @Override
    public int compareTo(Teller other) {
        return customersServed < other.customersServed ? -1 : 
            (customersServed == other.customersServed ? 0 : 1);
    }
}

class TellerManager implements Runnable {
    private ExecutorService exec;
    private CustomerLine customers;
    private PriorityQueue<Teller> workingTellers = new PriorityQueue<Teller>();
    private Queue<Teller> tellerDoingOtherThings = new LinkedList<Teller>();
    private int adjustmentPeriod;
    //private static Random rand = new Random(47);
    
    public TellerManager(ExecutorService e, CustomerLine customers, int adjustmentPeriod) {
        exec = e;
        this.customers = customers;
        this.adjustmentPeriod = adjustmentPeriod;
        
        // Start with a single teller:
        Teller teller = new Teller(customers);
        exec.execute(teller);
        workingTellers.add(teller);
    }
    
    public void adjustTellerNumber() {
        // This is actually a control system. By adjusting the numbers, you can reveal stability issues
        // in the control mechanism. If line is too long, add another teller
        if(customers.size() / workingTellers.size() > 2) {
            // if tellers are on break or doing another job, bring one back:
            if(tellerDoingOtherThings.size() > 0) {
                Teller teller = tellerDoingOtherThings.remove(); //Retrieves and removes the head of this queue. This method differs from poll only in that it throws an exception if this queue is empty.
                teller.serveCustomerLine();
                workingTellers.offer(teller);
                return;
            }
            
            // Else create (hire) a new teller
            Teller teller = new Teller(customers);
            exec.execute(teller);
            workingTellers.add(teller);
            return;
        }
        
        // If line is short enough, remove a teller:
        if(workingTellers.size() > 1 && customers.size() / workingTellers.size() < 2) {
            reassignOneTeller();
        }
        
        // If there is no line, we only need one teller
        if(customers.size() == 0 ) {
            while (workingTellers.size() > 1) {
                reassignOneTeller();
            }
        }
    }
    
    // Give a teller a different job or a break:
    private void reassignOneTeller() {
        Teller teller = workingTellers.poll(); // teller will be null if queue is empty
        teller.doSomethingElse();
        tellerDoingOtherThings.offer(teller);
    }
    
    @Override
    public void run() {
        try {
            
            while(!Thread.interrupted()) {
                TimeUnit.MILLISECONDS.sleep(adjustmentPeriod);
                adjustTellerNumber();
                
                print(customers + " { ");
                for(Teller teller : workingTellers) {
                    print(teller.shortString() + " ");
                }
                
                print("}");
            }
            
        } catch (Exception e) {
            print(this + " interrupted");
        }
        
        print(this + " ternimating");
    }
    
    public String toString() { return this.getClass().getSimpleName(); }
}

/** Use queues and multi-threading.
 * @author yzhou-citrix
 *
 */
public class BankTellerSimulation {

	static final int MAX_LINE_SIZE = 50;
	static final int ADJUSTMENT_PERIOD = 1000;
	
	public static void main(String[] args) throws NumberFormatException, InterruptedException, IOException {
		ExecutorService exec = Executors.newCachedThreadPool();
		
		// If line is too long, customers will leave
		CustomerLine customers = new CustomerLine(MAX_LINE_SIZE);
		
		exec.execute(new CustomerGenerator(customers));
		
		// Manager will add and remove tellers as necessary
		exec.execute(new TellerManager(exec, customers, ADJUSTMENT_PERIOD));
		
		if(args.length > 0) {
			TimeUnit.SECONDS.sleep(new Integer(args[0]));
		} else {
			print("Press 'Enter' to quit");
			System.in.read();
		}
		
		exec.shutdownNow();
	}
}
